package com.hockeo.tinygds;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

/**
 * Represents the interface between user code and the Google App Engine Datastore. You can
 * obtain an instance of this class by calling {@link TinyGDSFactory}
 *
 * @version $Id$
 * @author jjanke
 */
public class TinyGDS
{
  private final DatastoreService d_gds;
  private final Transaction      d_txn;
  private final TinyGDSFactory   d_factory;

  /**
   * Creates a new TinyGDS object tied to a transaction.
   *
   * @param gds the Google {@link DatastoreService} to use
   * @param factory the factory that created this object
   * @param txn an open transaction to be used for the following operations
   */
  TinyGDS( DatastoreService gds, TinyGDSFactory factory, Transaction txn )
  {
    d_gds = gds;
    d_factory = factory;
    d_txn = txn;
  }

  /**
   * Creates a new TinyGDS object that is not tied to any transaction.
   *
   * @param gds the Google {@link DatastoreService} to use
   * @param factory the factory that created this object
   */
  TinyGDS( DatastoreService gds, TinyGDSFactory factory )
  {
    this( gds, factory, null );
  }

  /**
   * Returns the currently active transaction or <code>null</code> if there is none.
   */
  protected Transaction getTransaction()
  {
    return d_txn;
  }

  /**
   * Returns the native Google DatastoreService which is used to execute datastore
   * operations.
   */
  protected DatastoreService getDatastoreService()
  {
    return d_gds;
  }

  /**
   * Retrieves the set of entities matching keys. The result Map will only contain Keys
   * for which Entities could be found.
   *
   * @param <T> the top level entity class for which to retrieve objects
   * @param itTKeys an Iterable containing TKey objects identifying the instances to be
   *          retrieved
   * @return a Map containing all objects that have been found for the given keys
   */
  public <T> Map<TKey<? extends T>, T> get( Iterable<TKey<? extends T>> itTKeys )
  {
    // transform all the keys into raw keys and save the TKeys in a map as we need them
    // later to figure out the actual entity class of the returned entities
    //
    Map<Object, TKey<? extends T>> mapTKeys = new HashMap<Object, TKey<? extends T>>();
    List<Key> listRawKeys = new ArrayList<Key>();
    for ( TKey<? extends T> tkey : itTKeys )
    {
      listRawKeys.add( d_factory.tKeyToRawKey( tkey ) );
      mapTKeys.put( tkey.getIdOrName(), tkey );
    }

    // get the matching entities from the datastore and transform into typed map
    //
    Map<Key, Entity> mapEnt = d_gds.get( d_txn, listRawKeys );
    Map<TKey<? extends T>, T> map = new HashMap<TKey<? extends T>, T>( mapEnt.size(), 1.0f );

    for ( Map.Entry<Key, Entity> entry : mapEnt.entrySet() )
    {
      Key keyRaw = entry.getKey();
      TKey<? extends T> tkey = mapTKeys.get( keyRaw.getName() != null ? keyRaw.getName() : keyRaw.getId() );
      TMetaData<? extends T> metadata = d_factory.getMetaData( tkey );

      map.put( tkey, metadata.toObject( entry.getValue() ) );
    }

    return map;
  }

  /**
   * Retrieves an entity matching the given key from the datastore.
   *
   * @param <T> the type of the class the searched entity belongs to
   * @param key the key to be used for querying the entity
   * @return the matching entity
   * @throws EntityNotFoundException if there is no object with the given key in the
   *           datastore
   */
  public <T> T get( TKey<T> key ) throws EntityNotFoundException
  {
    Entity ent = d_gds.get( d_txn, d_factory.tKeyToRawKey( key ) );

    return d_factory.getMetaData( key ).toObject( ent );
  }

  /**
   * Retrieves an entity of the given persistent class and having the given Id.
   *
   * @param <T> the type of the class the searched entity belongs to
   * @param cls the class the searched entity belongs to
   * @param lId the Id of the searched entity
   * @return the matching entity
   * @throws EntityNotFoundException if there is no object with the given Id belonging to
   *           the specified class in the datastore
   */
  public <T> T get( Class<T> cls, long lId ) throws EntityNotFoundException
  {
    return get( new TKey<T>( cls, lId ) );
  }

  /**
   * Retrieves an entity of the given persistent class and having the given Id.
   *
   * @param <T> the type of the class the searched entity belongs to
   * @param cls the class the searched entity belongs to
   * @param lId the Id of the searched entity
   * @return the matching entity
   * @throws EntityNotFoundException if there is no object with the given Id belonging to
   *           the specified class in the datastore
   */
  public <T> T get( Class<T> cls, String strId ) throws EntityNotFoundException
  {
    return get( new TKey<T>( cls, strId ) );
  }

  /**
   * Retrieves an entity matching the given key from the datastore.
   *
   * @param <T> the type of the class the searched entity belongs to
   * @param key the key to be used for querying the entity
   * @return the matching entity or <code>null</code> if there is none
   */
  public <T> T find( TKey<T> key )
  {
    Entity ent;

    try
    {
      ent = d_gds.get( d_txn, d_factory.tKeyToRawKey( key ) );
    }
    catch ( EntityNotFoundException ex )
    {
      return null;
    }

    return d_factory.getMetaData( key ).toObject( ent );
  }

  /**
   * Retrieves an entity of the given persistent class and having the given Id.
   *
   * @param <T> the type of the class the searched entity belongs to
   * @param cls the class the searched entity belongs to
   * @param lId the Id of the searched entity
   * @return the matching entity or <code>null</code> if there is none
   */
  public <T> T find( Class<T> cls, long lId )
  {
    return find( new TKey<T>( cls, lId ) );
  }

  /**
   * Retrieves an entity of the given persistent class and having the given Id.
   *
   * @param <T> the type of the class the searched entity belongs to
   * @param cls the class the searched entity belongs to
   * @param lId the Id of the searched entity
   * @return the matching entity or <code>null</code> if there is none
   */
  public <T> T find( Class<T> cls, String strId )
  {
    return find( new TKey<T>( cls, strId ) );
  }

  /**
   * Saves the given object in the datastore. If the object already exists, it is updated.
   * If the entity has no complete key, the key will be completed.
   *
   * @param <T> the type of the entity to be persisted
   * @param obj the object to be saved
   * @return the key of the persisted object
   */
  @SuppressWarnings( "unchecked" )
  public <T> TKey<T> put( T obj )
  {
    TMetaData<T> metadata = (TMetaData<T>) d_factory.getMetaData( obj.getClass() );
    Entity ent = metadata.toEntity( obj );

    Key keyRaw = d_gds.put( d_txn, ent );

    // set key to object so that any potentially generated id is properly propagated to
    // the persisted object
    //
    metadata.setKey( obj, keyRaw );

    return d_factory.rawKeyToTKey( keyRaw, metadata.getEntityClass() );
  }

  /**
   * Persists the objects in the given Iterable. If an object already exists it is updated
   * otherwise newly inserted. If an entity has no complete key, the key will be
   * completed.
   *
   * @param <T> the type of the entities to be persisted
   * @param itObj the Iterable containing the objects to be persisted
   * @return the keys of the persisted objects
   */
  @SuppressWarnings( "unchecked" )
  public <T> List<TKey<T>> put( Iterable<? extends T> itObj )
  {
    List<Entity> listEnt = new ArrayList<Entity>();
    for ( T obj : itObj )
    {
      TMetaData<T> metadata = (TMetaData<T>) d_factory.getMetaData( obj.getClass() );
      listEnt.add( metadata.toEntity( obj ) );
    }

    List<Key> listRawKeys = d_gds.put( d_txn, listEnt );
    List<TKey<T>> listTKeys = new ArrayList<TKey<T>>();
    Iterator<Key> itRawKeys = listRawKeys.iterator();
    for ( T obj : itObj )
    {
      Key keyRaw = itRawKeys.next();
      TMetaData<T> metadata = (TMetaData<T>) d_factory.getMetaData( obj.getClass() );
      TKey<T> tkey = d_factory.rawKeyToTKey( keyRaw, metadata.getEntityClass() );

      // set key to object so that any potentially generated id is properly propagated to
      // the persisted object
      //
      metadata.setKey( obj, keyRaw );

      // collect keys of all persisted objects
      //
      listTKeys.add( tkey );
    }

    return listTKeys;
  }

  /**
   * Deletes the entity that matches the given key from the datastore.
   *
   * @param tkey the key of the entity to be deleted
   */
  public void delete( TKey<?> tkey )
  {
    d_gds.delete( d_txn, d_factory.tKeyToRawKey( tkey ) );
  }

  /**
   * Deletes the object from the datastore.
   *
   * @param obj the object to be deleted
   */
  public void delete( Object obj )
  {
    TMetaData<?> metadata = d_factory.getMetaData( obj.getClass() );

    d_gds.delete( d_txn, metadata.getKey( obj ) );
  }

  /**
   * Deletes all the given objects from the datastore.
   *
   * @param aobj the objects to be deleted
   */
  public void deleteMultiple( Object... aobj )
  {
    deleteMultiple( aobj );
  }

  /**
   * Deletes all the given objects inside the Iterable from the datastore.
   *
   * @param itObjs the objects to be deleted
   */
  public void deleteMultiple( Iterable<Object> itObjs )
  {
    List<Key> listRawKeys = new ArrayList<Key>();

    for ( Object obj : itObjs )
    {
      TMetaData<?> metadata = d_factory.getMetaData( obj.getClass() );

      listRawKeys.add( metadata.getKey( obj ) );
    }

    d_gds.delete( d_txn, listRawKeys );
  }

  /**
   * Deletes all the entities matching the given keys from the datastore.
   *
   * @param atkey the keys of the entities to be deleted
   */
  public void deleteMultipleByKey( TKey<?>... atkey )
  {
    deleteMultipleByKey( atkey );
  }

  /**
   * Deletes all the entities matching the given keys from the datastore.
   *
   * @param atkey the keys of the entities to be deleted
   */
  public void deleteMultipleByKey( Iterable<TKey<?>> itTKeys )
  {
    List<Key> listRawKeys = new ArrayList<Key>();

    for ( TKey<?> tkey : itTKeys )
      listRawKeys.add( d_factory.tKeyToRawKey( tkey ) );

    d_gds.delete( d_txn, listRawKeys );
  }

  /**
   * Creates a new query that allows to query datastore objects of the given class.
   *
   * @param <T> the type of objects to be returned by the created query
   * @param cls class object representing the objects to be queried
   * @return a new query object
   */
  public <T> TQuery<T> query( Class<T> cls )
  {
    return new TQuery<T>( d_factory, this, cls );
  }
}
